/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.model;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import offset.nodes.Constants;
import offset.nodes.client.virtual.model.jcr.QName;
import offset.nodes.client.virtual.model.jcr.nodetype.NodeDefinition;
import offset.nodes.client.virtual.model.jcr.nodetype.NodeTypeDefinition;
import offset.nodes.client.virtual.model.jcr.nodetype.PropertyDefinition;

/**
 *
 * @author Walter Lütgenau
 */
public class NodeTypes implements NodeTypeResolver {
    NodeTypeDefinition[] originalDefinitions;
    Properties namespaceProperties = new Properties();
    HashMap nodeTypes = new HashMap();
    
    /** Creates a new instance of NodeTypes */
    public NodeTypes(NodeTypeDefinition[] definitions) {
        originalDefinitions = definitions;
        for( int i=0; i<definitions.length; i++) {
            definitions[i].setNodeTypeResolver(this);
            nodeTypes.put(definitions[i].getName(), definitions[i].clone());
        }
        InputStream input = SimpleNamespaceRegistry.class.getResourceAsStream("namespaces.properties");
        try {
            namespaceProperties.load(input);
        } catch( Exception e) {
            e.printStackTrace();
        }
        
        addNodesDefinitions();        
    }
    
    public NodeTypeDefinition get(QName name) {
        return (NodeTypeDefinition) nodeTypes.get(name);
    }
    
    public boolean contains(QName name) {
        return get(name) != null;
    }
    
    /**
     * A name maybe fix, if there is another node type, that has it as its child and has a fixed name for the child.
     * 
     * Note, that this is not certain, as the node maybe used as a root node or as a child of other node types, where it may 
     * have any name.
     * 
     * This method simply ensures, that there will be no exceptions when trying to persist the node type after it has given an arbitrary name.
     * 
     * @param name
     * @return
     */
    public boolean nameMaybeFix(QName name) {
        for( int i=0; i<originalDefinitions.length; i++) {
            if ( originalDefinitions[i].getDeclaredChildNodeDefinitions() != null) {
                NodeDefinition[] childDefinitions = originalDefinitions[i].getDeclaredChildNodeDefinitions();
                for( int j=0; j<childDefinitions.length; j++) {
                    if ( childDefinitions[j].getRequiredPrimaryTypes()[0].equals(name) && !childDefinitions[j].getName().equals(QName.ANY_NAME))
                        return true;
                }
            }
        }
        
        return false;
    }
    
    interface NodeTypeFilter {
        public boolean matches(NodeTypeDefinition nodeTypeDefinition);
    }
    
    class QNamesFilter implements NodeTypeFilter {
        QName[] filter;
        
        public QNamesFilter(QName[] filter) {
            this.filter = filter;
        }

        public boolean matches(NodeTypeDefinition nodeTypeDefinition) {
            for( int i=0; i<filter.length; i++) {
                boolean found = nodeTypeDefinition.getName().getNamespaceURI().equals(filter[i].getNamespaceURI());
                if ( found )
                    return true;
            }
            
            return false;
        }
    }
    
    class QNameFilter implements NodeTypeFilter {
        QName filter;
        
        public QNameFilter(QName filter) {
            this.filter = filter;
        }

        public boolean matches(NodeTypeDefinition nodeTypeDefinition) {
            return nodeTypeDefinition.getName().getNamespaceURI().equals(filter.getNamespaceURI());
        }
    }
    
    public NodeTypeDefinition[] getArray() {
        return getDefinitions(null);
    }
   
    public NodeTypeDefinition[] getArray(QName nameFilter) {
        return getDefinitions(new QNameFilter(nameFilter));
    }
   
    public NodeTypeDefinition[] getArray(QName[] nameFilters) {
        return getDefinitions(new QNamesFilter(nameFilters));
    }
   
   public NodeTypeDefinition[] getMixinTypes() {
       return getDefinitions( new NodeTypeFilter() {

            public boolean matches(NodeTypeDefinition nodeTypeDefinition) {
                return nodeTypeDefinition.isMixin();
            }
           
       });
   }

    
    public NodeTypeDefinition[] getDefinitions(NodeTypeFilter filter) {
        List types = null;
        if ( filter != null) {
            Iterator i = nodeTypes.values().iterator();
            types = new LinkedList();
            while( i.hasNext()) {
                NodeTypeDefinition nodeType = (NodeTypeDefinition) i.next();
                if (filter.matches(nodeType))
                    types.add(nodeType);
            }
        } else
            types = new ArrayList(nodeTypes.values());
        
        Collections.sort(types, new Comparator() {
            public int compare(Object o1, Object o2) {
                NodeTypeDefinition n1 = (NodeTypeDefinition) o1;
                NodeTypeDefinition n2 = (NodeTypeDefinition) o2;
                return n1.getName().getLocalName().compareTo(n2.getName().getLocalName());
            }
        });
        
        NodeTypeDefinition[] result = (NodeTypeDefinition[]) types.toArray(new NodeTypeDefinition[types.size()]);
        
        return result;
    }
    
    public void setType(NodeTypeDefinition type) {
        // add type
        nodeTypes.put(type.getName(), type);
    }
    
    public void removeType(QName typeName) {
        nodeTypes.remove(typeName);
    }
    
    public void setChildNode( QName name, NodeDefinition child) {
        NodeTypeDefinition type = get(name);
        if ( type == null)
            return;
        
        int index;
        NodeDefinition[] nodeDefinitions = type.getDeclaredChildNodeDefinitions();
        if ( (index = childIndex( nodeDefinitions, child)) >= 0) {
            nodeDefinitions[index] = child;
            type.setChildNodeDefs(nodeDefinitions);
            return;
        }
        
        NodeDefinition[] defs = new NodeDefinition[type.getDeclaredChildNodeDefinitions().length+1];
        for( int i=0; i<type.getDeclaredChildNodeDefinitions().length; i++) {
            defs[i] = type.getDeclaredChildNodeDefinitions()[i];
        }
        
        defs[type.getDeclaredChildNodeDefinitions().length] = child;
        type.setChildNodeDefs(defs);
    }
    
    public void removeChildNode( QName typeName, NodeDefinition child) {
        NodeTypeDefinition type = get(typeName);
        if ( type.getDeclaredChildNodeDefinitions() == null || type.getDeclaredChildNodeDefinitions().length == 0)
            return;
        
        if ( type.getDeclaredChildNodeDefinitions() == null || type.getDeclaredChildNodeDefinitions().length == 0)
            return;
        
        int childIndex = childIndex(type.getDeclaredChildNodeDefinitions(), child);
        if ( childIndex < 0)
            return;
        
        NodeDefinition[] defs = new NodeDefinition[type.getDeclaredChildNodeDefinitions().length-1];
        for( int i=0,j=0; i<type.getDeclaredChildNodeDefinitions().length; i++) {
            if ( i == childIndex)
                continue;
            defs[j++] = type.getDeclaredChildNodeDefinitions()[i];
        }
        
        type.setChildNodeDefs(defs);
    }
    
    public void setProperty( QName name, PropertyDefinition property) {
        NodeTypeDefinition type = get(name);
        
        int index;
        if ( (index = propertyIndex( type, property.getName())) >= 0) {
            PropertyDefinition[] propertyDefs =  type.getDeclaredPropertyDefinitions();
            propertyDefs[index] = property;
            type.setPropertyDefs(propertyDefs);
            return;
        }
        
        
        PropertyDefinition[] defs = new PropertyDefinition[type.getDeclaredPropertyDefinitions().length+1];
        for( int i=0; i<type.getDeclaredPropertyDefinitions().length; i++) {
            defs[i] = type.getDeclaredPropertyDefinitions()[i];
        }
        
        defs[type.getDeclaredPropertyDefinitions().length] = property;
        type.setPropertyDefs(defs);
    }
    
    public void removeProperty( QName typeName, QName property) {
        NodeTypeDefinition type = get(typeName);
        if ( type.getDeclaredPropertyDefinitions() == null || type.getDeclaredPropertyDefinitions().length == 0)
            return;
        
        if ( type.getDeclaredPropertyDefinitions() == null || type.getDeclaredPropertyDefinitions().length == 0)
            return;
        
        if ( propertyIndex(type, property) < 0)
            return;
        
        PropertyDefinition[] defs = new PropertyDefinition[type.getDeclaredPropertyDefinitions().length-1];
        for( int i=0,j=0; i<type.getDeclaredPropertyDefinitions().length; i++) {
            if ( type.getDeclaredPropertyDefinitions()[i].getName().equals(property))
                continue;
            defs[j++] = type.getDeclaredPropertyDefinitions()[i];
        }
        
        type.setPropertyDefs(defs);
    }
    
    protected int childIndex( NodeDefinition[] definitions, NodeDefinition child) {
        for (int i = 0; i < definitions.length; i++) {
            if (definitions[i].getName().equals(child.getName()) &&
                    definitions[i].getRequiredPrimaryTypes()[0].getNamespaceURI().equals(child.getRequiredPrimaryTypes()[0].getNamespaceURI()) &&
                    definitions[i].getRequiredPrimaryTypes()[0].getLocalName().equals(child.getRequiredPrimaryTypes()[0].getLocalName())  )
                return i;
        }
        
        return -1;
    }
    
    protected int propertyIndex( NodeTypeDefinition type, QName property) {
        for( int i=0; i<type.getDeclaredPropertyDefinitions().length; i++)
            if ( type.getDeclaredPropertyDefinitions()[i].getName().equals(property))
                return i;
        
        return -1;
    }
    
    public NodeTypeModifications getModifications() {
        NodeTypeModifications modifications = new NodeTypeModifications();
        
        // determine removed node types
        List removed = new LinkedList();
        for( int i=0; i<originalDefinitions.length; i++) {
            if ( !contains(originalDefinitions[i].getName()))
                removed.add(originalDefinitions[i]);
        }
        modifications.setRemoved((NodeTypeDefinition[])removed.toArray(new NodeTypeDefinition[removed.size()]));
        
        // determine added node types
        List added = new LinkedList();
        Iterator it = nodeTypes.values().iterator();
        while( it.hasNext()) {
            NodeTypeDefinition nodeType = (NodeTypeDefinition) it.next();
            for( int i=0; i<originalDefinitions.length; i++) {
                if ( nodeType.getName().equals(originalDefinitions[i].getName())) {
                    nodeType = null;
                    break;
                }
            }
            if ( nodeType != null)
                added.add(nodeType);
        }
        modifications.setAdded((NodeTypeDefinition[])added.toArray(new NodeTypeDefinition[added.size()]));
        
        // determine modified node types
        List modified = new LinkedList();
        it = nodeTypes.values().iterator();
        while( it.hasNext()) {
            NodeTypeDefinition nodeType = (NodeTypeDefinition) it.next();
            for( int i=0; i<originalDefinitions.length; i++) {
                if ( nodeType.getName().equals(originalDefinitions[i].getName())) {
                    if ( !nodeType.equals(originalDefinitions[i]) ) {
                        modified.add(nodeType);
                        break;
                    }
                }
                
            }
        }
        modifications.setModified((NodeTypeDefinition[])modified.toArray(new NodeTypeDefinition[modified.size()]));
        
        return modifications;
    }
    
    public void addNodesDefinitions() {
        if ( contains(new QName(Constants.URI_NODES_DATA, Constants.LOCALNAME_DATA)))
            return;
        
        NodeTypeDefinition type = new NodeTypeDefinition();
        type.setName(new QName(Constants.URI_NODES_DATA, Constants.LOCALNAME_DATA));
        type.setSupertypes(new QName[] {QName.NT_HIERARCHY_NODE});
        
        nodeTypes.put(type.getName(), type);
    }
    
    /**
     * Retrieve the node type definition of this prefix name.
     * 
     * @param prefixName the prefix name
     * @return the node type definition or null
     */
    public NodeTypeDefinition getNodeType(String prefixName) {
        NodeTypeDefinition result = get(SimpleNamespaceRegistry.getInstance().toQName(prefixName));
        return result;
    }
    
    /**
     * Retrieve the property definition of this combination of node type definition and property prefix name.
     * 
     * @param nodeType the node type definition
     * @param propertyPrefixName the property prefix name
     * @return the property definition or null
     */
    public PropertyDefinition getPropertyDefinition(NodeTypeDefinition nodeType, String propertyPrefixName) {
        for (PropertyDefinition definition : nodeType.getPropertyDefinitions()) {
            QName name = SimpleNamespaceRegistry.getInstance().toQName(propertyPrefixName);
            if (name == null)
                return null;
            
            if (definition.getName().equals(name))
                return definition;
        }
        
        return null;
    }
    
}
